Profile picture

[Spring] Spring DTO의 데이터 바인딩과 직렬화

Amaranth2023년 12월 04일

서론


커디 프로젝트의 코드리뷰를 진행하다 Json 직렬화/역직렬화 과정과 관련하여 새롭게 알게 된 개념이 좀 있었습니다. 그 개념들을 머릿속에 정리하고 기록으로 남기고자 글을 작성합니다.

기술부채 모음이어서 소제목 간 큰 흐름이 있지는 않습니다.

기술부채


데이터 바인딩과 직렬화

여러 자료를 찾아보다 보니 데이터 바인딩과 직렬화의 개념이 헷갈려서 한 번 개념을 확실히 짚고 넘어가기로 했습니다.

데이터 바인딩은 어떤 데이터의 값을 다른 형식의 데이터로 매핑하는 프로세스입니다. 스프링 MVC와 같은 웹 프레임워크의 맥락에서 봤을 때 데이터 바인딩은 HTTP 요청의 Parameter, Body 데이터를 Java 객체로 변환하는 프로세스를 의미합니다.

직렬화는 객체를 Json, Xml과 같이 쉽게 전송할 수 있는 형식의 데이터로 변환하는 프로세스를 의미합니다. 역직렬화는 그 반대 프로세스를 의미합니다.

데이터 바인딩 == 역직렬화?

우리가 늘 작성하는 코드로 생각을 했을 때, @RequestBody로 받은 데이터가 DTO 객체에 매핑되는 것은 데이터 바인딩이 이루어진 결과라고 할 수도 있고, 역직렬화의 결과라고 표현할 수도 있습니다.

두 개념의 차이는, 데이터바인딩이 역직렬화의 개념을 포괄하는 더 넓은 개념이라는 것입니다.

데이터 바인딩 과정에서 발생할 수 있는 예외의 종류

MethodArgumentNotValidException

: @Valid, @Validate 바인딩 과정에서 에러가 발생한 경우

예를 들어 다음과 같은 DTO 객체와, 해당 DTO를 파라미터로 받는 API 핸들러 메서드가 있다고 합시다.

@Getter
@RequiredArgsConstructor
public class TestRequest {

	@NotBlank
	private final String name;

}
@PostMapping("/test") 
public ResponseEntity<Void> test(@RequestBody @Valid TestRequest request) {
      ...
}

여기서 다음과 같이 name 값을 비워서 요청을 보내면 MethodArgumentNotValidException 예외가 발생합니다.

POST localhost:8080/test
Content-Type: application/json

{
	name : ""
}

BindException

: @ModelAttribute 바인딩 과정에서 에러가 발생한 경우

MethodArgumentTypeMismatchException

: enum 타입 불일치로 인해 바인딩 에러가 발생한 경우

예를 들어 다음과 같은 enum과 이 enum 타입의 데이터를 파라미터로 받는 API 핸들러 메서드가 있다고 합시다.

public enum EventType {  
  CONFERENCE,  
  COMPETITION  
}
@GetMapping  
public ResponseEntity<List<EventResponse>> findEvents(  
    @RequestParam(required = false) final EventType category,
    ...) {  
  ... 
}

여기서 다음과 같이 enum 타입과 맞지 않는 데이터(CONNNNFERENCE)를 요청으로 보냈을 때 발생하는 것이 바로 MethodArgumentTypeMismatchException입니다.

GET http://localhost:8080/events?category=CONNNNFERENCE  
Content-Type: application/json

참고 게시글

@DateTimeFormat과 @JsonFormat

@DateTimeFormatSpring에서 지원하는 어노테이션으로, LocalDate, LocalDateTime과 같은 날짜 관련 타입의 직렬화를 지원하는 어노테이션입니다. @JsonFormatJackson 라이브러리에서 지원하는 어노테이션으로, LocalDate, LocalDateTime과 같은 날짜 관련 타입의 데이터를 JSON으로 직렬화할 때의 포맷을 관리하는 어노테이션입니다.

  • Request
    • @ModelAttribute - @DateTimeFormat만 사용 가능
    • @RequestParameter - @DateTimeFormat만 사용 가능
    • @RequestBody - 둘 다 사용 가능
  • Response Body - @JsonFormat만 사용 가능

위에서 정리한 내용에 따르면, RequestBody를 직렬화할 때는 @DateTimeFormat@JsonFormat를 둘 다 사용할 수 있습니다. 여기서 만약 두 어노테이션을 같이 사용할 경우, @JsonFormat이 우선적으로 적용됩니다.

요약하면, GET 요청의 파라미터로 날짜 데이터를 받아올 경우 @DateTimeFormat을 사용해야 하고, POST 요청의 Request Body나 Response Body로 날짜 데이터를 Json으로 파싱할 하는 경우 @JsonFormat을 사용하면 됩니다.

참고 게시글

@RequestBody, @RequestParam 어노테이션 없이 DTO 객체만을 받을 때

GET 요청의 경우

: DTO 필드 각각을 @RequestParam으로 선언한 것과 동일하게 처리됩니다.

예를 들어 DTO와 GET 요청에 대한 API 핸들러 메서드를 다음과 같이 작성했다고 합시다.

public class TestDto {
	private final String name;
	private final Integer age;
}
@GetMapping("/example") 
public String handleGetRequest(TestDto request) { 
	// Business logic 
	return "viewName"; 
}

이 경우, 다음과 같은 요청을 정상적으로 바인딩합니다.

GET localhost:8080/example?name=value1&age=42

다르게 말하자면, 앞에서 작성한 핸들러 메서드는 다음의 메서드와 동일하게 동작합니다.

@GetMapping("/example") 
public String handleGetRequest(@RequestParam final String name, @RequestParam final Integer age) { 
	// Business logic 
	return "viewName"; 
}

POST나 PUT 요청의 경우

: DTO 앞에 @RequestBody을 선언해준 경우와 동일하게 동작합니다.

예를 들어 앞서 작성한 DTO를 가지고 다음과 같은 POST 메서드를 작성했다고 합시다.

@PostMapping("/example") 
public void handlePostRequest(TestDto request) { 
	// Business logic 
}

이 경우, 다음과 같은 요청을 정상적으로 바인딩합니다.

POST localhost:8080/example

{
	name: "아마란스",
	age: "23"
}

DTO 직렬화/역직렬화

Spring은 ObjectMapper라는 클래스를 사용해 Json 값을 Spring의 자바 객체로 변환합니다.

Spring이 객체를 Json 데이터로 직렬화하기 위해서는 객체에 getter(또는 setter)가 선언되어 있어야 합니다.

=>Response Dto에 getter를 작성해야 하는 이유

반대로 Spring이 Json 데이터를 Spring 객체로 역직렬화(바인딩)하기 위해서는 객체에 기본 생성자와 getter(또는 setter)가 선언되어 있어야 합니다.

=>Request Dto에 getter, 기본 생성자를 작성해야 하는 이유

하지만, 실제로는 @RequestBody로 역직렬화를 진행하는 경우 기본 생성자 없이도 역직렬화가 수행됩니다. 그 이유는 spring-boot-starter-web 의존성에 포함된 jackson-module-parameter-names 모듈의 ParameterNamesModule 클래스가 JsonCreator를 사용해 기본 생성자가 없는 객체도 역직렬화될 수 있도록 설정해주기 때문입니다.

RequestBody DTO의 필드가 1개인 경우


그런데 여기서, 역직렬화하려는 대상 DTO의 필드가 1개인 경우 JSON parse error가 발생합니다. 이유는 ParameterNamesModules 클래스가 해당 DTO 객체를 생성할 때 JsonCreator의 Mode를 어떤 것으로 설정해야 할 지 알 수 없기 때문입니다.

이 문제를 해결하기 위해서는 DTO 객체에 기본 생성자를 추가하거나, 정의한 생성자에 @JsonCreator를 붙이면 됩니다.

참고 게시글

@JsonIgnore 어노테이션


public class UserResponse {

	...
	@JsonIgnore
	private final LocalDateTime createAt;
}

Json 직렬화 시 특정 필드를 포함시키고 싶지 않을 때 사용하는 어노테이션. 해당 어노테이션을 사용하면 DTO Response에 해당 필드가 제외됩니다.


Loading script...